#!/usr/bin/python
# -*- coding: utf-8 -*-
# vim: ai ts=4 sts=4 et sw=4
#
# Courier filter that normalizes From: headers to a canonical form:
# From: cn <mail>
# Author: Aleksander Adamowski <aleksander.adamowski at olo.org.pl>
# Based-on: Pawel Sawicki's isoCourierFilter (Pawel J. Sawicki <p-j-s@p-j-s.name>)
# s/-//g
# $Id: fromNormalizer.py 70 2008-06-12 22:58:36Z olo $
#
############################################################################
#                                                                          #
#   This program is free software; you can redistribute it and/or modify   #
#   it under the terms of the GNU General Public License as published by   #
#   the Free Software Foundation; either version 2 of the License, or      #
#   (at your option) any later version.                                    #
#                                                                          #
############################################################################


import courier.config
import courier.control
import courier.xfilter
import ldap
import re
import codecs
import time
import sys
import os
import os.path
import stat
import email
from email.MIMEText import MIMEText
from email.Header import Header
import pickle
import thread
import gc

reNonAlnumChars                     = re.compile('[^a-zA-Z0-9]')
# Cannot place in config file as the constant is defined in external module:
ldapSearchScope     = ldap.SCOPE_SUBTREE                    # search scope
ldapMutex           = thread.allocate_lock()

def initFilter():
    global exemptRecipientDomains
    global exemptSenderDomains
    global ldapServerURI
    global ldapBindDN
    global ldapBindPassword
    global reExemptRecipientDomain
    global reExemptSenderDomain
    global ldapConnection
    courier.config.applyModuleConfig('courier-pythonfilters-ldap', globals())
    courier.config.applyModuleConfig('fromNormalizer.py', globals())
    reExemptRecipientDomain = re.compile("[^@]+@("+("|".join(exemptRecipientDomains))+")", re.I)
    reExemptSenderDomain = re.compile("[^@]+@("+("|".join(exemptSenderDomains))+")", re.I)
    ldapMutex.acquire()
    try:
      # connect to ldap server
      ldapConnection      = ldap.initialize(ldapServerURI)
      # bind synchronously
      ldapConnection.simple_bind_s(ldapBindDN, ldapBindPassword)
    except ldap.LDAPError, error_message:
        debug_log('LDAP operation error during init: %s' % error_message)
        ldapMutex.release()
    if ldapMutex.locked():
      ldapMutex.release()
    # say hello
    sys.stderr.write('Initialized the "fromNormalizer" python filter.\n')

def doFilter(bodyFile, controlFileList):
#    try:
        global ldapConnection
        global ldapServerURI
        global ldapBaseDN
        global ldapAttributes
        global attribPrefixes
        global ldapFilter
        global ldapMailAttributes
        global ldapMailFilter
        global reExemptRecipientDomain
        global reExemptSenderDomain
        global reNonAlnumChars
        global ldapSearchScope


        """
        Get the message from the queue, but if it has already been processed,
        stop processing and leave it alone:
        """
        try:
            msgfilter = courier.xfilter.XFilter('fromNormalizerFilter', bodyFile,
                controlFileList)
        except courier.xfilter.LoopError, e:
            debug_log('Message already filtered')
            # LoopError indicates that we've already filtered this message.
            return ''

        msg = msgfilter.getMessage()

        # get the authenticated user:
        authUser = determine_auth_user(controlFileList, bodyFile, msg)
        if authUser is None:
            debug_log('No authentication data. Terminating.')
            return ''

        """
        Check if all recipients' adresses belong to an exempt recipient domain.
        If at least one non-exempt address is present in the recipient table -
        append the signature:
        """
        isNonExemptRecipientPresent = False

        # get the recipients table
        recipients = courier.control.getRecipients(controlFileList)

        # find any non-exempt adresses
        for recipient in recipients:
            if not reExemptRecipientDomain.match(recipient.lower()):
                #debug_log('non exempt recipient found: ' + recipient)
                isNonExemptRecipientPresent = True


        """
        If all recipients are in exempt domains, stop processing:
        """
        if not isNonExemptRecipientPresent:
            debug_log('no non-exempt recipients, exiting.')
            return ''

        # prepare local ldap filter
        localLdapFilter = ldapFilter % { 'uid': authUser }

        (ldapResultType, ldapResultData) = threadsafe_ldap_search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapAttributes)

        if len(ldapResultData) != 1 or not ldapResultType == ldap.RES_SEARCH_RESULT:
            # user hasn't been found in the LDAP directory
            debug_log('no LDAP entry or ambiguous search result for %s.' % localLdapFilter)
            debug_log(pickle.dumps(ldapResultData))
            return '' 

        ldapEntryAttrs = ldapResultData[0][1]
        canonicalAddress = ''
        if 'mail' in ldapEntryAttrs:
            canonicalAddress = ldapEntryAttrs['mail'][0].lower()
            debug_log('Canonical address :' + canonicalAddress)
        else:
            debug_log('No "mail" attribute for %s! Terminating!' % authUser)
            return ''


        """
        Check if the sender isn't exempted from processing.
        If he is, stop processing:
        """
        # get the sender
        #emailSender = courier.control.getSender(controlFileList).lower()
        emailSender = canonicalAddress
        #debug_log('sender: ' + emailSender)

        if reExemptSenderDomain.match(emailSender):
            debug_log('sender is exempt, exiting.')
            return ''


        # get the message

        fromSender = msg.get_all('from')
        debug_log('fromSender: %s' % fromSender)
        (mailSenderRealName, mailSenderEmail) = email.Utils.getaddresses(fromSender)[0]
        newMailSenderRealName = []
        for elem in email.Header.decode_header(mailSenderRealName):
            if not elem[1] is None:
                newMailSenderRealName.append(unicode(elem[0], elem[1]))
            else:
                newMailSenderRealName.append(unicode(elem[0], 'us-ascii'))
        mailSenderRealName = unicode(" ".join(newMailSenderRealName))
        debug_log('Senders real name: %s, e-mail: %s' % (mailSenderRealName, mailSenderEmail))

        senderAddrOK = True
        senderRealNameOK = True
        senderRealNameDontCheck = False

        if mailSenderEmail.lower() == canonicalAddress.lower():
            debug_log(mailSenderEmail + ' is a permitted sender address.')
            senderAddrOK = True
        else:
            """
            altkomPermittedSenderAddresses attribute, if present, designates addresses which the sender
            is permitted to send mail from:
            """
            if 'altkomPermittedSenderAddresses' in ldapEntryAttrs:
                altkomPermittedSenderAddresses = ldapEntryAttrs['altkomPermittedSenderAddresses']
                if mailSenderEmail in altkomPermittedSenderAddresses or mailSenderEmail == canonicalAddress:
                    debug_log(mailSenderEmail + ' is a permitted sender address.')
                    senderRealNameDontCheck = True
                    senderAddrOK = True
                else:
                    senderAddrOK = False
            else:
                debug_log('no altkomPermittedSenderAddresses found for %s and sender address %s is not canonical.' % (localLdapFilter, mailSenderEmail))
                senderAddrOK = False

        if 'cn' in ldapEntryAttrs and (not senderRealNameDontCheck):
            if mailSenderRealName.lower() in [unicode(elem, 'utf-8').lower() for elem in ldapEntryAttrs['cn']]:
                debug_log(mailSenderRealName + ' is a permitted sender real name.')
                senderRealNameOK = True
            else:
                debug_log('Real name "%s" is not OK for sender %s, should be in "%s".' % (mailSenderRealName.lower(), localLdapFilter, " ".join([unicode(elem, 'utf-8').lower() for elem in ldapEntryAttrs['cn']])))
                senderRealNameOK = False
        else:
                debug_log('no canonical name found for sender %s!!!' % localLdapFilter)

        if (not senderAddrOK) or (not senderRealNameOK):
            if (not senderAddrOK):
                correctedSenderAddr = canonicalAddress

            else:
                correctedSenderAddr = mailSenderEmail
            if (not senderRealNameOK):
                correctedSenderRealName = unicode(ldapEntryAttrs['cn'][0], 'utf-8')
            else:
                correctedSenderRealName = mailSenderRealName
            #debug_log('correcting sender to: "%s", <%s>' % (unicode(correctedSenderRealName,'utf-8'), correctedSenderAddr))
            correctedSenderRealName = str(Header(correctedSenderRealName, 'utf-8'))
            correctedFrom = email.Utils.formataddr((correctedSenderRealName, correctedSenderAddr))
            msg.replace_header('From', correctedFrom)
            debug_log('correcting sender to: "%s"' % correctedFrom)
            #correctedFromHeader = Header(unicode(correctedFrom), 'utf-8')
            #msg.replace_header('From', correctedFromHeader.encode())
            

        try:
          msgfilter.submit()
          debug_log("fromNormalizer.py has successfully filtered the message " + msg.get('message-id'))
        except courier.xfilter.SubmitError, e:
          debug_log('error submitting message: %s' % str(e))
          del msg
          del msgfilter
        del msg
        del msgfilter
        # Return 250, no more filters should be run on this copy.
        if courier.config.isMinVersion('0.57.1'):
          return ''
        else:
          # Old courier version, submit created a new message.
          # Return 250, no more filters should be run on this copy.
          return '050 OK'

#    except Exception, e:
        # all exceptions are ignored by the python filter, so it is safe to raise them here
#        raise e

def debug_log(message):
    if debug_logging:
        sys.stderr.write((__name__ + ': ' + message + '\n').encode('utf-8'))

def threadsafe_ldap_search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapAttributes):
    global ldapConnection
    global ldapMutex
    ldapMutex.acquire()
    try:
        for tryNumber in range(ldapRetries):
            if tryNumber == ldapRetries:
                debug_log('Giving up after %d LDAP reconnection attempts.' % ldapRetries)
                return ''
            try:
                # get the signature template id from LDAP:
                ldapResultId = ldapConnection.search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapAttributes)
                ldapResultType, ldapResultData = ldapConnection.result(ldapResultId)
                break
            except ldap.LDAPError, error_message:
                debug_log('LDAP operation error on try %d out of %d: %s' % (tryNumber + 1, ldapRetries, error_message))
                time.sleep(1)
                if tryNumber != ldapRetries:
                    debug_log('retrying LDAP operation')
                try:
                    del ldapConnection
                    ldapConnection = ldap.initialize(ldapServerURI)
                    ldapConnection.simple_bind_s(ldapBindDN, ldapBindPassword)
                except ldap.LDAPError, error_message:
                    debug_log('LDAP reconnection error on try %d out of %d: %s' % (tryNumber + 1, ldapRetries, error_message))
    finally:
        ldapMutex.release()

    if ldapMutex.locked():
        ldapMutex.release()
        debug_log('Unreleased ldapMutex detected!')
    return (ldapResultType, ldapResultData)

def mail2uid(mailAddress):
    localLdapFilter = ldapMailFilter % { 'mail': mailAddress }
    debug_log('Searching for auth based on 127.0.0.1 sender: ' + localLdapFilter)
    (ldapResultType, ldapResultData) = threadsafe_ldap_search(ldapBaseDN, ldapSearchScope, localLdapFilter, ldapMailAttributes)
    if len(ldapResultData) != 1 or not ldapResultType == ldap.RES_SEARCH_RESULT:
        debug_log('no LDAP entry or ambiguous search result for %s.' % localLdapFilter)
        debug_log(pickle.dumps(ldapResultData))
        del localLdapFilter
        del ldapResultType
        del ldapResultData
        return None
    else:
        mailUID = ldapResultData[0][1]['uid'][0]
        debug_log('Found 127.0.0.1 authUser: ' + mailUID)
        del localLdapFilter
        del ldapResultType
        del ldapResultData
        return mailUID

def determine_auth_user(controlFileList, bodyFile, msg):
    authUser = courier.control.getAuthUser(controlFileList, bodyFile)
    if authUser:
        if msg.has_key('X-Pythonfilters-Auth'):
            msg.replace_header('X-Pythonfilters-Auth', authUser)
        else:
            msg.add_header('X-Pythonfilters-Auth', authUser)
    else:
        if msg.has_key('X-Pythonfilters-Auth'):
            authUser = msg.get('X-Pythonfilters-Auth')
        else:
            if courier.control.getSendersIP(controlFileList) == '127.0.0.1':
                """
                Typical webmail installations don't use SMTP AUTH, but the sender address
                in such cases can usually be trusted and searched on.
                """
                authUser = mail2uid(courier.control.getSender(controlFileList))
            else:
                return None
    return authUser

